/*******************************************************************************
 * Copyright 2011-2012 Dik Grapendaal
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package sh.grapendaal.tsuushin.core;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.WriteCompletionEvent;
import org.jboss.netty.handler.timeout.IdleState;
import org.jboss.netty.handler.timeout.IdleStateAwareChannelHandler;
import org.jboss.netty.handler.timeout.IdleStateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import sh.grapendaal.tsuushin.message.IncomingMessage;
import sh.grapendaal.tsuushin.message.JoinMessage;
import sh.grapendaal.tsuushin.message.MessageFactory;
import sh.grapendaal.tsuushin.message.MessageType;
import sh.grapendaal.tsuushin.message.NickMessage;
import sh.grapendaal.tsuushin.message.OutgoingMessage;
import sh.grapendaal.tsuushin.message.PongMessage;
import sh.grapendaal.tsuushin.message.UserMessage;

public class IrcBot extends IdleStateAwareChannelHandler implements Runnable {
	private static final Logger LOGGER = LoggerFactory.getLogger(IrcBot.class);

	private static final String LINE_ENDING = "\r\n";

	/**
	 * This enumeration contains all the known disconnect reasons that might
	 * occur in the application's lifetime.
	 */
	public enum DisconnectReason {
		/**
		 * Indicates an unexpected disconnection.
		 */
		CONNECTION_RESET,

		/**
		 * Indicates a cleanly closed connection.
		 */
		CONNECTION_CLOSED,

		/**
		 * Indicates that the remote party did not respond in a timely manner.
		 */
		CONNECTION_TIMEOUT
	}

	private Network network;
	private ClientBootstrap bootstrap;
	private IrcClient client;
	private BotInformation information = new BotInformation();
	private List<Channel> channels = new ArrayList<Channel>();
	private List<Listener> listeners = new ArrayList<Listener>();

	public static class BotInformation {
		private String nickname;
		private String username;
		private String realname;
		private String disconnectReason;

		public String getNickname() {
			return nickname;
		}

		public void setNickname(String nickname) {
			this.nickname = nickname;
		}

		public String getUsername() {
			return username;
		}

		public void setUsername(String username) {
			this.username = username;
		}

		public String getRealname() {
			return realname;
		}

		public void setRealname(String realname) {
			this.realname = realname;
		}

		public String getDisconnectReason() {
			return disconnectReason;
		}

		public void setDisconnectReason(String disconnectReason) {
			this.disconnectReason = disconnectReason;
		}

		@Override
		public String toString() {
			return "BotInformation [nickname=" + nickname + ", username=" + username + ", realname=" + realname + "]";
		}
	}

	public Network getNetwork() {
		return network;
	}

	public void setNetwork(Network network) {
		this.network = network;
	}
	
	public ClientBootstrap getBootstrap() {
		return bootstrap;
	}

	public void setBootstrap(ClientBootstrap bootstrap) {
		this.bootstrap = bootstrap;
	}

	/**
	 * Use this getter to get a reference to the BotInformation object of this
	 * bot and update its nickname, username and real name.
	 * 
	 * @return The BotInformation object currently in use.
	 */
	public BotInformation getBotInformation() {
		return information;
	}

	public List<Channel> getChannels() {
		return channels;
	}

	public void setChannels(List<Channel> channels) {
		this.channels = channels;
	}

	public void addChannel(String channelName) {
		this.channels.add(new Channel(channelName));
	}

	public void addChannel(String channelName, String password) {
		this.channels.add(new Channel(channelName, password));
	}

	public void addChannel(Channel channel) {
		this.channels.add(channel);
	}

	public void removeChannel(String channelName) {
		this.channels.remove(new Channel(channelName));
	}

	public void removeChannel(Channel channel) {
		this.channels.remove(channel);
	}

	public void connect() throws IOException {
		client = new IrcClient(bootstrap, network);
		client.connect();
	}

	public void disconnect() {
		client.close();
	}

	@Override
	public void run() {
		try {
			connect();
		} catch (IOException e) {
			LOGGER.error("Could not setup client.", e);
		}
	}

	/**
	 * Wrapper method to easily send OutgoingMessage objects.
	 * 
	 * @param message
	 *            The message to send.
	 */
	public void send(OutgoingMessage message) {
		client.send(message.toOutgoingMessage());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void channelConnected(ChannelHandlerContext context, ChannelStateEvent event) {
		dispatchConnect();
		
		NickMessage nick = new NickMessage();
		nick.setNickname(information.getNickname());
		send(nick);

		UserMessage user = new UserMessage();
		user.setUsername(information.getUsername());
		user.setRealname(information.getRealname());
		send(user);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void messageReceived(ChannelHandlerContext channelHandlerContext, MessageEvent event) {
		IncomingMessage message;
		try {
			message = MessageFactory.create((String) event.getMessage());
		} catch (Exception e) {
			LOGGER.error("Exception while parsing.", e);
			return;
		}

		LOGGER.debug("Parsed message: {}", message.getClass());

		if (message.getMessageType() == MessageType.PING) {
			PongMessage pong = new PongMessage();
			pong.setDestination(message.getContent());
			send(pong);
		}

		if (message.getMessageType() == MessageType.WELCOME_MESSAGE) {
			for (Channel channel : channels) {
				JoinMessage join = new JoinMessage();
				join.setChannel(channel.getName());
				join.setPassword(channel.getPassword());
				send(join);
			}
		}

		if (message.getMessageType() == MessageType.NICKNAME_IN_USE) {
			information.setNickname(information.getNickname() + "_");
			NickMessage newNickname = new NickMessage();
			newNickname.setNickname(information.getNickname());
			send(newNickname);
		}

		dispatchReceive(message);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext context, ExceptionEvent event) {
		LOGGER.error("Exception caught in IrcClientHandler", event.getCause());

		event.getChannel().close();
		dispatchDisconnect(DisconnectReason.CONNECTION_RESET);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void channelDisconnected(ChannelHandlerContext context, ChannelStateEvent event) {
		LOGGER.info("Disconnected from IRC server.");

		dispatchDisconnect(DisconnectReason.CONNECTION_CLOSED);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void channelIdle(ChannelHandlerContext context, IdleStateEvent event) {
		if (event.getState() == IdleState.READER_IDLE) {
			event.getChannel().close();
			dispatchDisconnect(DisconnectReason.CONNECTION_TIMEOUT);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void disconnectRequested(ChannelHandlerContext context, ChannelStateEvent event) {
		if (context.getChannel().isConnected()) {
			String disconnectReason = this.information.disconnectReason != null ? " :"
					+ this.information.disconnectReason : "";
			event.getChannel().write("QUIT" + disconnectReason + LINE_ENDING);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * Attaches the sent message in the channel handler context, so that the
	 * {@link writeComplete()} knows which line was sent and can fire off the
	 * onSend() event.
	 */
	@Override
	public void writeRequested(ChannelHandlerContext context, MessageEvent event) {
		context.setAttachment(event.toString());

		try {
			super.writeRequested(context, event);
		} catch (Exception e) {
			throw new TsuushinException(e);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * Dispatches the message as set in the {@link writeRequested()} method to
	 * the listeners.
	 */
	@Override
	public void writeComplete(ChannelHandlerContext context, WriteCompletionEvent event) {
		LOGGER.debug("Sent line: {}", (String) context.getAttachment());

		dispatchSend((String) context.getAttachment());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void handleUpstream(ChannelHandlerContext context, ChannelEvent event) {
		LOGGER.debug(event.toString());

		try {
			super.handleUpstream(context, event);
		} catch (Exception e) {
			throw new TsuushinException(e);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void handleDownstream(ChannelHandlerContext context, ChannelEvent event) {
		LOGGER.debug(event.toString());

		try {
			super.handleDownstream(context, event);
		} catch (Exception e) {
			throw new TsuushinException(e);
		}
	}

	/**
	 * Adds an object that wants to receive events from the Socket class.
	 * 
	 * @param listener
	 *            The listener to add.
	 */
	public void addListener(Listener listener) {
		listeners.add(listener);
	}

	/**
	 * Removes the listener object that was added.
	 * 
	 * @param listener
	 *            The listener to remove.
	 */
	public void removeListener(Listener listener) {
		listeners.remove(listener);
	}

	/**
	 * Dispatches the connect event to all registered listeners.
	 */
	private void dispatchConnect() {
		for (Listener listener : listeners) {
			if (listener instanceof ConnectListener) {
				((ConnectListener) listener).onConnect(this);
			}
		}
	}

	/**
	 * Dispatches the receive event to all registered listeners.
	 * 
	 * @param line
	 *            The line that was received.
	 */
	private void dispatchReceive(IncomingMessage message) {
		for (Listener listener : listeners) {
			if (listener instanceof ReceiveListener && !((ReceiveListener) listener).onReceive(this, message)) {
				break;
			}
		}
	}

	/**
	 * Dispatches the send event to all registered listeners.
	 * 
	 * @param line
	 *            The line that was sent.
	 */
	private void dispatchSend(String line) {
		for (Listener listener : listeners) {
			if (listener instanceof SendListener && !((SendListener) listener).onSend(this, line)) {
				break;
			}
		}
	}

	/**
	 * Dispatches the disconnect event to all registered listeners.
	 * 
	 * @param reason
	 *            The reason as to why we're disconnected.
	 */
	private void dispatchDisconnect(DisconnectReason reason) {
		for (Listener listener : listeners) {
			if (listener instanceof DisconnectListener) {
				((DisconnectListener) listener).onDisconnect(this, reason);
			}
		}
	}

	/**
	 * This interface is the base interface for any interface that defines a
	 * certain IrcBot event.
	 */
	private interface Listener {

	}

	public interface ConnectListener extends Listener {
		/**
		 * Gets called when the bot successfully connected.
		 * 
		 * @param bot
		 *            The current bot.
		 */
		void onConnect(IrcBot bot);
	}

	public interface ReceiveListener extends Listener {
		/**
		 * Gets called when a single line was received. The line does not
		 * include line endings. The method should return true, except if there
		 * is sensitive data in the message, then false should be returned. This
		 * will prevent any further listeners from receiving the event.
		 * 
		 * @param bot
		 *            The current bot.
		 * @param message
		 *            The parsed received message.
		 * @return Boolean which indicates whether we should continue
		 *         processing.
		 */
		boolean onReceive(IrcBot bot, IncomingMessage message);
	}

	public interface SendListener extends Listener {
		/**
		 * Gets called when a single line was sent. The line does not include
		 * line endings. The method should return true, except if there is
		 * sensitive data in the message, then false should be returned. This
		 * will prevent any further listeners from receiving the event.
		 * 
		 * @param bot
		 *            The current bot.
		 * @param line
		 *            The sent line.
		 * @return Boolean which indicates whether we should continue
		 *         processing.
		 */
		boolean onSend(IrcBot bot, String line);
	}

	public interface DisconnectListener extends Listener {
		/**
		 * Gets called when the bot was disconnected. The disconnect reason is
		 * supplied, which can used to determine whether we want to reconnect
		 * the bot or not.
		 * 
		 * @param bot
		 *            The current bot.
		 * @param reason
		 *            The reason as to why we're disconnected.
		 */
		void onDisconnect(IrcBot bot, DisconnectReason reason);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {
		return "IrcBot [network=" + network + ", information=" + information + "]";
	}
}